/**
 * @Date: Feb 19, 2010 1:18:42 PM
 */
package com.philip.journal.home.service;

import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import com.philip.journal.common.BeanUtils;
import com.philip.journal.common.StringUtils;
import com.philip.journal.core.Constant;
import com.philip.journal.core.Messages;
import com.philip.journal.core.Messages.Error;
import com.philip.journal.core.bean.AbstractAuditableBean;
import com.philip.journal.core.bean.User;
import com.philip.journal.core.exception.JournalException;
import com.philip.journal.core.service.BaseService;
import com.philip.journal.home.bean.Branch;
import com.philip.journal.home.bean.ConfigItem;
import com.philip.journal.home.bean.Entry;
import com.philip.journal.home.dao.BranchDAO;
import com.philip.journal.home.dao.EntryDAO;

/**
 * @author cry30
 */
public class HomeServiceImpl extends BaseService implements HomeService {

    /** Used only by the getPreferences. Can be refactored out if getPreferences is moved to its own interface/impl. */
    static final String CHILDREN = "children";

    /** {@link #getBranchNodeProperty(StringBuilder, long)} or {@link #getEntryNodeProperty(StringBuilder, long)}. */
    static final int IDX_BEAN = 0;
    /** {@link #getBranchNodeProperty(StringBuilder, long)} or {@link #getEntryNodeProperty(StringBuilder, long)}. */
    static final int IDX_SIZE = 1;
    /** {@link #getBranchNodeProperty(StringBuilder, long)} or {@link #getEntryNodeProperty(StringBuilder, long)}. */
    static final int IDX_CREATED = 0;
    /** {@link #getBranchNodeProperty(StringBuilder, long)} or {@link #getEntryNodeProperty(StringBuilder, long)}. */
    static final int IDX_MODIFIED = 1;

    /** Default complete date format for Home Service display. */
    private static final DateFormat COMP_DISP_FMT = new SimpleDateFormat( // NOPMD by r39 on 3/30/11 2:36 PM
            "EEEE, dd MMM, yyyy, hh:mm:ss a", Locale.getDefault());

    /** RTFC. */
    private static final String FMT_DATE_ONLY = "yyyy-MM-dd";
    /** RTFC. */
    private static final String FMT_TIME_ONLY = "HH:mm:ss:SSS";

    /** Default complete date format for Home Service to parse from view. */
    private static final DateFormat COMP_PARSE_FMT = new SimpleDateFormat(FMT_DATE_ONLY + " " + FMT_TIME_ONLY, Locale // NOPMD by r39 on 4/4/11 5:32 PM
            .getDefault());

    /** Synchronization lock object. */
    private static Object lock = new Object();

    @Override
    public String getEntryTreePath(final Map<String, Object> session, final Entry entry)
    {
        if (entry == null) {
            throw JournalException.wrapperException(new IllegalArgumentException(Error.IAE_NULL));
        }

        final StringBuilder strBuilder = new StringBuilder();
        strBuilder.append(Constant.ENTRYID_PREFIX + entry.getNodeId());
        strBuilder.insert(0, getBranchPath(session, entry.getBranch()));
        return strBuilder.toString();
    }

    /**
     * RTFC. TODO: Unit Test.
     *
     * @param session - server side session.
     * @param branch branch to which to extract the path.
     * @return Branch path.
     */
    String getBranchPath(final Map<String, Object> session, final Branch branch)
    {
        final StringBuilder strBuilder = new StringBuilder();
        Branch parent = branch;
        strBuilder.insert(0, Constant.BRANCHID_PREFIX + parent.getBranchId());
        if (!Constant.ROOT_NAME.equals(parent.getName())) {
            do {
                parent = parent.getParent();
                strBuilder.insert(0, Constant.BRANCHID_PREFIX + parent.getBranchId());
            } while (!Constant.ROOT_NAME.equals(parent.getName()));
        }

        return strBuilder.toString();
    }

    @Override
    public long addBranch(final Map<String, Object> session, final long parentId, final String branchName)
    {
        final Branch parentBranch = getDaoFacade().getBranchDAO().read(parentId);
        if (parentBranch == null) {
            throw new JournalException(Messages.Error.JE_INV_BRANCHID);
        } else if (StringUtils.getInstance().isNullOrEmpty(branchName)) {
            throw new JournalException(Messages.Error.JE_INV_BRANCHNAME);
        }

        final Branch branch = new Branch(branchName, parentBranch);
        branch.setCreator((User) session.get(Constant.CURRENT_USER));
        getDaoFacade().getBranchDAO().save(branch);
        return branch.getBranchId();
    }

    @Override
    public void deleteBranch(final Map<String, Object> session, final long branchId)
    {
        if (branchId == Constant.ROOT_ID) {
            throw new JournalException(Messages.Error.JE_INV_BRANCHID);
        }
        final Branch branch = getDaoFacade().getBranchDAO().read(branchId);
        if (branch == null) {
            throw new JournalException(Messages.Error.JE_INV_BRANCHID);
        }

        final List<Branch> withSubBranches = new ArrayList<Branch>();
        withSubBranches.add(branch);
        final List<Branch> children = getChildren(branch);
        if (children != null && !children.isEmpty()) {
            withSubBranches.addAll(children);
        }

        for (final Branch next : withSubBranches) {
            final List<Entry> entries = getDaoFacade().getEntryDAO().readAllByBranch(next.getBranchId());
            if (entries != null) {
                getDaoFacade().getEntryDAO().deleteAll(entries);
            }
        }
        getDaoFacade().getBranchDAO().deleteAll(withSubBranches);
    }

    /**
     * Factored out method to get Children of Branch.
     *
     * @param parent Branch to get Children from.
     * @return Children of given Branch.
     */
    private List<Branch> getChildren(final Branch parent)
    {
        final List<Branch> retval = getDaoFacade().getBranchDAO().readAllByParent(parent.getBranchId());
        Collections.sort(retval, new Comparator<Branch>() {
            @Override
            public int compare(final Branch branch1, final Branch branch2)
            {
                return branch1.getName().compareToIgnoreCase(branch2.getName());
            }
        });
        return retval;
    }

    @Override
    public void renameBranch(final Map<String, Object> session, final long branchId, final String newName)
    {
        final Branch branch = getDaoFacade().getBranchDAO().read(branchId);
        if (branch == null) {
            throw new JournalException(Messages.Error.JE_INV_BRANCHID);
        }

        branch.setName(newName);
        getDaoFacade().getBranchDAO().save(branch);
    }

    @Override
    public List<Branch> getChildren(final Map<String, Object> session, final long parentId)
    {
        return getDaoFacade().getBranchDAO().readAllByParent(parentId);
    }

    @Override
    public void moveBranch(final Map<String, Object> session, final long newParentId, final long branchId)
    {
        final BranchDAO branchDao = getDaoFacade().getBranchDAO();
        final Branch branch = branchDao.read(branchId);
        final Branch parent = branchDao.read(newParentId);

        if (branch == null || parent == null) {
            throw new JournalException(Messages.Error.JE_INV_BRANCHID);
        }
        final String newIds = Constant.BRANCHID_PREFIX + newParentId;
        final String branchIds = Constant.BRANCHID_PREFIX + branchId;
        final String branchPath = getBranchPath(session, parent);
        final int newIdx = branchPath.indexOf(newIds);
        final int parentIdx = branchPath.indexOf(branchIds);

        if (parentIdx > -1 && newIdx > parentIdx) {
            throw new JournalException(Messages.Error.JE_INV_BRANCHID);
        }

        branch.setParent(parent);
        branchDao.save(branch);
    }

    @Override
    public Map<String, Object> getNodeProperties(final Map<String, Object> session, final long nodeId,
            final boolean isEntry)
    {
        final Map<String, Object> retval = new HashMap<String, Object>();
        final StringBuilder path = new StringBuilder();

        final Object[] beanData = isEntry ? getEntryNodeProperty(path, nodeId) : getBranchNodeProperty(path, nodeId);
        final long beanSize = (Long) beanData[IDX_SIZE];
        final Date[] actionDates = getActionDates((AbstractAuditableBean) beanData[IDX_BEAN]);

        String parentName;
        Object[] parentBeanData = new Object[beanData.length];
        System.arraycopy(beanData, 0, parentBeanData, 0, beanData.length);
        Branch parent = null;
        do {
            parent = (Branch) BeanUtils.getProperty(parentBeanData[IDX_BEAN],
                    parentBeanData[IDX_BEAN] instanceof Entry ? Entry.PARENT : Branch.ATTR_PARENT);

            parentName = BeanUtils.getProperty(parent, Branch.ATTR_NAME).toString();
            path.insert(0, "/" + parentName);

            parentBeanData = getBranchNodeProperty(null, parent.getBranchId());
        } while (!parentName.equals(Constant.ROOT_NAME));

        synchronized (lock) {
            retval.put(Constant.NodeProperty.CREATED, COMP_DISP_FMT.format(actionDates[IDX_CREATED]));
            retval.put(Constant.NodeProperty.MODIFIED, COMP_DISP_FMT.format(actionDates[IDX_MODIFIED]));
        }
        final NumberFormat numFormat = NumberFormat.getIntegerInstance();
        numFormat.setGroupingUsed(true);
        retval.put(Constant.NodeProperty.SIZE, numFormat.format(beanSize));
        retval.put(Constant.NodeProperty.PATH, path.toString());
        return retval;
    }

    /**
     * Generate size, create and update date, path (/<title>). Update date when null will default to create date. <br/>
     * StringBuilder when null will simply ignore append of /<name>.
     *
     * Refactored out from {@link #getNodeProperties(Map, long, boolean)}.
     *
     * @see related {@link #getBranchNodeProperty(StringBuilder, long)}
     *
     * @param strBuilder String path to append Entry name.
     * @param nodeId ID of Entry.
     * @return {Entity, size}
     * @exception JournalException when the nodeId passed cannot be found.
     */
    Object[] getEntryNodeProperty(final StringBuilder strBuilder, final long nodeId)
    {
        final EntryDAO entryDao = getDaoFacade().getEntryDAO();
        final Entry entry = entryDao.read(nodeId);
        if (entry == null) {
            throw JournalException.wrapperException(new IllegalArgumentException(Messages.Error.IAE_NULL));
        }

        long size = entry.getTitle().length();
        if (entry.getDescription() != null) {
            size += entry.getDescription().length();
        }
        if (strBuilder != null) {
            strBuilder.append("/" + entry.getTitle());
        }
        return new Object[] {
                entry,
                size };
    }

    /**
     * Refactored out from {@link #getNodeProperties(Map, long, boolean)}. StringBuilder when null will simply ignore
     * append of /<name>.
     *
     * @see related {@link #getEntryNodeProperty(StringBuilder, long)}
     *
     * @param strBuilder String path to append Branch name.
     * @param branchId ID of Branch.
     * @return {Entity, size}
     * @exception JournalException branchId is not found.
     */
    Object[] getBranchNodeProperty(final StringBuilder strBuilder, final long branchId)
    {
        final BranchDAO branchDao = getDaoFacade().getBranchDAO();
        final Branch branch = branchDao.read(branchId);
        if (branch == null) {
            throw JournalException.wrapperException(new IllegalArgumentException(Messages.Error.IAE_NULL));
        }
        final long size = branch.getName() == null ? 0 : branch.getName().length();
        if (strBuilder != null) {
            strBuilder.append("/" + branch.getName());
        }
        return new Object[] {
                branch,
                size };
    }

    /**
     * TODO: Unit Testing. <br/>
     * Refactored code. Will return Create date on index 0, Update date on index 1.
     *
     * @param bean either Entry or Branch.
     * @return the action date array.
     */
    Date[] getActionDates(final AbstractAuditableBean bean)
    {
        final Date[] retval = new Date[2];
        final DateFormat dateFormat = new SimpleDateFormat(FMT_DATE_ONLY, Locale.getDefault());
        final DateFormat timeFormat = new SimpleDateFormat(FMT_TIME_ONLY, Locale.getDefault());

        if (bean.getCreateDate() != null && bean.getCreateTime() != null) {

            try {
                synchronized (lock) {
                    retval[IDX_CREATED] = COMP_PARSE_FMT.parse(dateFormat.format(bean.getCreateDate()) + " "
                            + timeFormat.format(bean.getCreateTime()));
                }
            } catch (final ParseException ignore) {
                getLogger().warn(ignore.getMessage(), ignore);
            }
        }
        if (bean.getUpdateDate() != null && bean.getUpdateTime() != null) {

            try {
                synchronized (lock) {
                    retval[IDX_MODIFIED] = COMP_PARSE_FMT.parse(dateFormat.format(bean.getUpdateDate()) + " "
                            + timeFormat.format(bean.getUpdateTime()));
                }
            } catch (final ParseException ignore) {
                getLogger().warn(ignore.getMessage(), ignore);
            }
        }

        return retval;
    }

    @Override
    public List<Map<String, Object>> getPreferences(final Map<String, Object> session)
    {
        final List<Map<String, Object>> retval = new ArrayList<Map<String, Object>>();

        final List<ConfigItem> allConfigItem = getDaoFacade().getConfigItemDAO().readAll();

        final Map<Long, List<ConfigItem>> parentChildMap = new HashMap<Long, List<ConfigItem>>();
        List<ConfigItem> children;
        final List<ConfigItem> parentList = new ArrayList<ConfigItem>();

        for (final ConfigItem configItem : allConfigItem) {
            if (configItem.getParent() == null) {
                parentList.add(configItem);
            } else {
                final long parentConfigId = configItem.getParent().getConfigId();
                if (parentChildMap.get(parentConfigId) == null) {
                    parentChildMap.put(parentConfigId, new ArrayList<ConfigItem>()); // NOPMD by r39
                }
                children = parentChildMap.get(parentConfigId);
                children.add(configItem);
            }

        }

        for (final ConfigItem parent : parentList) {
            retval.add(convertToMap(parent, parentChildMap));
        }
        return retval;
    }

    /**
     * Recursive helper method for converting ConfigItem to Map.
     *
     * @param configItem ConfigItem object to converted.
     * @param parentChildMap Parent to Children mapping.
     * @return Converted ConfigItem
     * @exception JournalException when any of the 2 parameter is null.
     */
    Map<String, Object> convertToMap(final ConfigItem configItem,
            final Map<Long, List<ConfigItem>> parentChildMap)
    {
        if (configItem == null || parentChildMap == null) {
            throw JournalException.wrapperException(new IllegalArgumentException(Messages.Error.IAE_NULL));
        }

        final Map<String, Object> retval = BeanUtils.convertToMap(configItem, new String[] {
                AbstractAuditableBean.CREATOR,
                AbstractAuditableBean.UPDATER });

        final List<ConfigItem> children = parentChildMap.get(configItem.getConfigId());
        List<Map<String, Object>> childrenMap = null;

        if (children != null) {
            childrenMap = new ArrayList<Map<String, Object>>();
            for (final ConfigItem child : children) {
                childrenMap.add(convertToMap(child, parentChildMap));
            }
            retval.put(CHILDREN, childrenMap);
        }
        return retval;
    }
}
